﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;

namespace DoubleAspect
{
    public static class Instrumentation
    {
        private static readonly IDictionary<string, Action<object[]>> BeforeActions = new Dictionary<string, Action<object[]>>();
        private static readonly IDictionary<string, Func<object[], object, object>> AfterActions = new Dictionary<string, Func<object[], object, object>>();

        public static void AddBeforeAction(string memberName, Action<object[]> action)
        {
            BeforeActions[memberName] = action;
        }

        public static void ClearBeforeActions()
        {
            BeforeActions.Clear();
        }

        public static void AddAfterAction(string memberName, Func<object[], object, object> action)
        {
            AfterActions[memberName] = action;
        }

        public static void ClearAfterActions()
        {
            AfterActions.Clear();
        }

        public static void BeforeMethod(string memberId, object[] parameters)
        {
            Action<object[]> action;
            if (!BeforeActions.TryGetValue(memberId, out action))
            {
                return;
            }
            action(parameters);
        }

        public static object AfterMethod(string memberId, object[] parameters, object returnObject)
        {
            Func<object[], object, object> action;
            if (!AfterActions.TryGetValue(memberId, out action))
            {
                return returnObject;
            }
            return action(parameters, returnObject);
        }

        public static void EncapsulateNonjittedMethod(MethodBase method)
        {

            //TODO different types of methods, ctor, (cctor unsupported) -> MethodBase
            var methodInfo = method as MethodInfo;
            if (methodInfo == null)
            {
                return;
            }
            IntPtr originalAddress = RetrieveMethodPointer(method);
            MethodBase encapsulation = EmitEncapsulatonMethod(methodInfo);
            IntPtr encapsulationAddress = RetrieveMethodPointer(encapsulation);
            ReplaceMethodPointer(originalAddress, encapsulationAddress);
        }

        private static unsafe void ReplaceMethodPointer(IntPtr originalAddress, IntPtr replacingMethod)
        {
            if (IntPtr.Size == 8)
            {
                var d = (ulong*)originalAddress.ToPointer();
                *d = *((ulong*)replacingMethod.ToPointer());
            }
            else
            {
                var d = (uint*)originalAddress.ToPointer();
                *d = *((uint*)replacingMethod.ToPointer());
            }
        }

        public static void Wuschel()
        {

        }

        private static MethodInfo EmitEncapsulatonMethod(MethodInfo method)
        {
            var beforeMethod = typeof(Instrumentation).GetMethod("BeforeMethod");
            var afterMethod = typeof(Instrumentation).GetMethod("AfterMethod");
            var wuschel = typeof(Instrumentation).GetMethod("Wuschel");

            //TODO all proxy methods and types in one assembly
            var proxyAssembly = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName("Proxies"), AssemblyBuilderAccess.RunAndSave);
            var proxyModule = proxyAssembly.DefineDynamicModule("Proxies.dll");

            // TODO generate uniqueName for method
            // TODO apply returning type
            // TODO apply arguments
            string methodId = method.Name;
            var parameters = method.GetParameters();
            var parameterTypes = (from t in parameters select t.ParameterType).ToArray();

            var copyType = proxyModule.DefineType("CopyType", TypeAttributes.Public | TypeAttributes.Class);
            var methodCopy = copyType.DefineMethod(method.Name, MethodAttributes.HideBySig | MethodAttributes.Public | MethodAttributes.Static,
                        method.ReturnType, parameterTypes);
            var g = methodCopy.GetILGenerator();
            g.Emit(OpCodes.Ret);
            var type1 = copyType.CreateType();
            var replacedMethod = type1.GetMethod(method.Name);
            var retrieveMethodPointer = RetrieveMethodPointer(replacedMethod);
            IntPtr originalAddress = RetrieveMethodPointer(method);
            ReplaceMethodPointer(retrieveMethodPointer, originalAddress);

            var proxyType = proxyModule.DefineType("ProxyType", TypeAttributes.Public | TypeAttributes.Class);

            var methodWithJunctionPoints = proxyType.DefineMethod(method.Name, MethodAttributes.HideBySig | MethodAttributes.Static | MethodAttributes.Public,
                                    method.ReturnType, parameterTypes);

            g = methodWithJunctionPoints.GetILGenerator();
            g.DeclareLocal(typeof(string));
            g.DeclareLocal(typeof(object[]));
            g.DeclareLocal(typeof(object));
            g.Emit(OpCodes.Ldstr, methodId);
            g.Emit(OpCodes.Stloc_0);
            g.Emit(OpCodes.Ldc_I4, parameters.Length);
            g.Emit(OpCodes.Newarr, typeof(object));
            g.Emit(OpCodes.Stloc_1);
            for (int i = 0; i < parameters.Length; i++)
            {
                g.Emit(OpCodes.Ldloc_1);
                g.Emit(OpCodes.Ldc_I4, i);
                g.Emit(OpCodes.Ldarg, i);

                //TODO if value type then box to reference type, before stelem_ref
                g.Emit(OpCodes.Stelem_Ref);
            }
            g.Emit(OpCodes.Ldloc_0);
            g.Emit(OpCodes.Ldloc_1);

            g.Emit(OpCodes.Call, beforeMethod);

            //TODO out and ref arguments
            //TODO prevent loop call
            bool hasReturnValue = false;
            /*for (int i = 0; i < parameters.Length; i++)
            {
                g.Emit(OpCodes.Ldarg, i);
            }
            g.Emit(OpCodes.Call, replacedMethod);*/
            if (method.ReturnType != typeof(void))
            {
                hasReturnValue = true;
                g.Emit(OpCodes.Stloc_2);
            }
            g.Emit(OpCodes.Ldloc_0);
            g.Emit(OpCodes.Ldloc_1);

            if (hasReturnValue)
            {
                g.Emit(OpCodes.Ldloc_2);
            }
            else
            {
                g.Emit(OpCodes.Ldnull);
            }
            g.Emit(OpCodes.Call, afterMethod);

            if (hasReturnValue)
            {
                g.Emit(OpCodes.Ldloc_2);
            }
            else
            {
                g.Emit(OpCodes.Pop);
            }
            g.Emit(OpCodes.Ret);

            var type = proxyType.CreateType();
            return type.GetMethod(method.Name);
        }

        private static IntPtr RetrieveMethodPointer(MethodBase method)
        {
            unsafe
            {
                return new IntPtr(((int*)method.MethodHandle.Value.ToPointer() + 2));
            }
        }
    }
}
